//   Copyright 2012,2013 Vaughn Vernon
//
//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
//       http://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing, software
//   distributed under the License is distributed on an "AS IS" BASIS,
//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
//   limitations under the License.

package com.saasovation.agilepm.domain.model.product;

import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

import com.saasovation.agilepm.domain.model.Entity;
import com.saasovation.agilepm.domain.model.discussion.DiscussionAvailability;
import com.saasovation.agilepm.domain.model.discussion.DiscussionDescriptor;
import com.saasovation.agilepm.domain.model.product.backlogitem.BacklogItem;
import com.saasovation.agilepm.domain.model.product.backlogitem.BacklogItemId;
import com.saasovation.agilepm.domain.model.product.backlogitem.BacklogItemStatus;
import com.saasovation.agilepm.domain.model.product.backlogitem.BacklogItemType;
import com.saasovation.agilepm.domain.model.product.backlogitem.StoryPoints;
import com.saasovation.agilepm.domain.model.product.release.Release;
import com.saasovation.agilepm.domain.model.product.release.ReleaseId;
import com.saasovation.agilepm.domain.model.product.sprint.Sprint;
import com.saasovation.agilepm.domain.model.product.sprint.SprintId;
import com.saasovation.agilepm.domain.model.team.ProductOwner;
import com.saasovation.agilepm.domain.model.team.ProductOwnerId;
import com.saasovation.agilepm.domain.model.tenant.TenantId;
import com.saasovation.common.domain.model.DomainEventPublisher;

/**
 * 产品继承自实体，实体继承自断言关注
 * (聚合体)
 */
public class Product extends Entity {

    /**
     * 产品ID（ProductId：值对象[用于存放实体的唯一标识]）
     */
    private ProductId productId;

    /**
     * 产品经理ID（ProductOwnerId：值对象）
     */
    private ProductOwnerId productOwnerId;

    /**
     * 租户Id
     */
    private TenantId tenantId;
    /**
     * 产品名
     */
    private String name;

    /**
     * 内容描述
     */
    private String description;

    /**
     * 产品待办事项清单（ProductBacklogItem：实体）
     */
    private Set<ProductBacklogItem> backlogItems;

    /**
     * 产品讨论
     */
    private ProductDiscussion discussion;

    /**
     * 获取产品描述
     * @return 返回产品描述
     */
    public String description() {
        return this.description;
    }
    /**
     * 发起创建产品领域事件的构造器
     * @param aTenantId                 租户ID
     * @param aProductId                产品ID
     * @param aProductOwnerId           产品经理ID
     * @param aName                     产品名
     * @param aDescription              内容描述
     * @param aDiscussionAvailability   讨论可用性
     */
    public Product(
            TenantId aTenantId,
            ProductId aProductId,
            ProductOwnerId aProductOwnerId,
            String aName,
            String aDescription,
            DiscussionAvailability aDiscussionAvailability) {

        // 调用默认构造器
        this();

        // 设置租户Id
        this.setTenantId(aTenantId); // 必须在productOwnerId之前进行比较
        // 设置内容描述
        this.setDescription(aDescription);
        // 根据讨论可用性，获取可用的产品讨论
        this.setDiscussion(ProductDiscussion.fromAvailability(aDiscussionAvailability));
        // 设置讨论发起ID为无效
        this.setDiscussionInitiationId(null);
        // 设置产品名
        this.setName(aName);
        // 设置产品ID
        this.setProductId(aProductId);
        // 设置产品经理ID
        this.setProductOwnerId(aProductOwnerId);

        //　核心功能：使用领域事件发布者的一个单例，来发布一个产品创建
        DomainEventPublisher
                .instance()
                .publish(new ProductCreated(
                        this.tenantId(),
                        this.productId(),
                        this.productOwnerId(),
                        this.name(),
                        this.description(),
                        this.discussion().availability().isRequested()));
    }
    /**
     * 获取所有待办事项清单
     * @return 返回待办事项清单
     */
    public Set<ProductBacklogItem> allBacklogItems() {
        return Collections.unmodifiableSet(this.backlogItems());
    }
    /**
     * 改变产品经理
     * @param aProductOwner 产品经理
     */
    public void changeProductOwner(ProductOwner aProductOwner) {
        // 只有产品经理ID和参数产品经理的ID不相同的情况下，
        if (!this.productOwnerId().equals(aProductOwner.productOwnerId())) {
            // 设置此产品经理ID为最新的产品经理ID
            this.setProductOwnerId(aProductOwner.productOwnerId());
            // 经理ＩＤ变更的情况下，有可能发生领域事件
            // TODO: publish event
        }
    }
    /**
     * 获取产品讨论
     * @return 返回产品讨论
     */
    public ProductDiscussion discussion() {
        return this.discussion;
    }
    /**
     * 讨论发起ID
     */
    private String discussionInitiationId;

    /**
     * 获取讨论发起ID
     * @return 返回讨论发起ID
     */
    public String discussionInitiationId() {
        return this.discussionInitiationId;
    }
    /**
     * 失败讨论发起
     */
    public void failDiscussionInitiation() {
        // 只有在没准备就绪的情况下，才进行处理；否则不进行处理
        if (!this.discussion().availability().isReady()) {
            // 设置讨论发起ＩＤ为无效
            this.setDiscussionInitiationId(null);

            // 设置产品讨论为可用性失败
            this.setDiscussion(
                    ProductDiscussion
                            .fromAvailability(
                                    DiscussionAvailability.FAILED));
        }
    }
    /**
     * 发起讨论
     * @param aDescriptor 讨论描述
     */
    public void initiateDiscussion(DiscussionDescriptor aDescriptor) {
        // 只有讨论描述有效的情况下，才可以继续；否则报错
        if (aDescriptor == null) {
            throw new IllegalArgumentException("The descriptor must not be null.");
        }

        // 如果该讨论已经请求的情况下，发布领域事件
        if (this.discussion().availability().isRequested()) {

            // 把现在准备好的讨论描述赋值给产品讨论
            this.setDiscussion(this.discussion().nowReady(aDescriptor));

            // 核心功能：使用领域事件发布者的一个单例，来发布一个产品发起讨论
            DomainEventPublisher
                    .instance()
                    .publish(new ProductDiscussionInitiated(
                            this.tenantId(),
                            this.productId(),
                            this.discussion()));
        }
    }
    /**
     * 获取产品名
     * @return 返回产品名
     */
    public String name() {
        return this.name;
    }
    /**
     * 计划待办事项
     * @param aNewBacklogItemId 新待办事项ID
     * @param aSummary			摘要
     * @param aCategory			范围
     * @param aType				类型
     * @param aStoryPoints		故事点清单
     * @return					返回待办事项
     */
    public BacklogItem planBacklogItem(
            BacklogItemId aNewBacklogItemId,
            String aSummary,
            String aCategory,
            BacklogItemType aType,
            StoryPoints aStoryPoints) {

        // 生成待办事项，并把状态更新成已计划
        BacklogItem backlogItem =
                new BacklogItem(
                        this.tenantId(),
                        this.productId(),
                        aNewBacklogItemId,
                        aSummary,
                        aCategory,
                        aType,
                        BacklogItemStatus.PLANNED,
                        aStoryPoints);
        // 核心功能：使用领域事件发布者的一个单例，来发布一个计划好的产品待办事项
        DomainEventPublisher
                .instance()
                .publish(new ProductBacklogItemPlanned(
                        backlogItem.tenantId(),
                        backlogItem.productId(),
                        backlogItem.backlogItemId(),
                        backlogItem.summary(),
                        backlogItem.category(),
                        backlogItem.type(),
                        backlogItem.storyPoints()));
        // 返回一个计划好的产品待办事项
        return backlogItem;
    }

    /**
     * 计划产品待办事项
     * @param aBacklogItem 待办事项
     */
    public void plannedProductBacklogItem(BacklogItem aBacklogItem) {
        // 只有产品的租户ＩＤ和待办事项的租户ＩＤ相等，才能计划；否则报错
        this.assertArgumentEquals(this.tenantId(), aBacklogItem.tenantId(), "The product and backlog item must have same tenant.");
        // 只有产品的产品ID和待办事项清单的产品ＩＤ相等，才能计划；否则报错
        this.assertArgumentEquals(this.productId(), aBacklogItem.productId(), "The backlog item must belong to product.");

        // 首先，在当前待办事项清单的长度基础上，领取新的排序号
        int ordering = this.backlogItems().size() + 1;
        // 其次，根据租户ID、产品ID、待办事项ID、排序号，生成新的产品待办事项
        ProductBacklogItem productBacklogItem =
                new ProductBacklogItem(
                        this.tenantId(),
                        this.productId(),
                        aBacklogItem.backlogItemId(),
                        ordering);
        // 最后，把此待办事项追加到清单中
        this.backlogItems().add(productBacklogItem);
    }
    /**
     * 获取产品ID
     * @return 返回产品ID
     */
    public ProductId productId() {
        return this.productId;
    }

    /**
     * 获取产品经理ID
     * @return 返回产品经理ID
     */
    public ProductOwnerId productOwnerId() {
        return this.productOwnerId;
    }

    /**
     * 根据待办事项ID和排序号，对待办事项清单进行重新排序
     * @param anId 产品待办事项ID
     * @param anOrdering 排序号
     */
    public void reorderFrom(BacklogItemId anId, int anOrdering) {
        // 遍历所有产品待办事项清单，并进行重新排序
        for (ProductBacklogItem productBacklogItem : this.backlogItems()) {
            productBacklogItem.reorderFrom(anId, anOrdering);
        }
    }
    /**
     * 要求讨论
     * @param aDiscussionAvailability 讨论可用性
     */
    public void requestDiscussion(DiscussionAvailability aDiscussionAvailability) {
        // 产品讨论请求的领域事件发布的条件：在讨论可用性方面，只有没准备就绪才发给一个讨论发起ID
        if (!this.discussion().availability().isReady()) {

            // 根据可用性，获取一个产品讨论的值对象；并设置好
            this.setDiscussion(
                    ProductDiscussion.fromAvailability(
                            aDiscussionAvailability));
            // 核心功能：使用领域事件发布者的一个单例，来发布一个产品讨论请求
            DomainEventPublisher
                    .instance()
                    .publish(new ProductDiscussionRequested(
                            this.tenantId(),
                            this.productId(),
                            this.productOwnerId(),
                            this.name(),
                            this.description(),
                            this.discussion().availability().isRequested()));
        }
    }
    /**
     * 计划发布
     * @param aNewReleaseId 新发布ＩＤ
     * @param aName　		发布名
     * @param aDescription	内容描述
     * @param aBegins		开始日期
     * @param anEnds		结束日期
     * @return				返回一个发布
     */
    public Release scheduleRelease(
            ReleaseId aNewReleaseId,
            String aName,
            String aDescription,
            Date aBegins,
            Date anEnds) {
        // 首先根据（租户ID、产品ID、新发布ID、发布名、内容描述、开始和结束日期，来生产一个冲刺
        Release release =
                new Release(
                        this.tenantId(),
                        this.productId(),
                        aNewReleaseId,
                        aName,
                        aDescription,
                        aBegins,
                        anEnds);

        // 核心功能：使用领域事件发布者的一个单例，来发布一个产品发布计划
        DomainEventPublisher
                .instance()
                .publish(new ProductReleaseScheduled(
                        release.tenantId(),
                        release.productId(),
                        release.releaseId(),
                        release.name(),
                        release.description(),
                        release.begins(),
                        release.ends()));
        // 然后，把发布好的产品发布计划返回
        return release;
    }
    /**
     * 计划冲刺
     * @param aNewSprintId	新冲刺ID
     * @param aName			冲刺名
     * @param aGoals		目标
     * @param aBegins		开始日期
     * @param anEnds		结束日期
     * @return	返回一个冲刺
     */
    public Sprint scheduleSprint(
            SprintId aNewSprintId,
            String aName,
            String aGoals,
            Date aBegins,
            Date anEnds) {
        // 首先根据（租户ID、产品ID、新冲刺ID、冲刺名、目标、开始和结束日期，来生产一个冲刺
        Sprint sprint =
                new Sprint(
                        this.tenantId(),
                        this.productId(),
                        aNewSprintId,
                        aName,
                        aGoals,
                        aBegins,
                        anEnds);

        // 核心功能：使用领域事件发布者的一个单例，来发布一个产品冲刺计划
        DomainEventPublisher
                .instance()
                .publish(new ProductSprintScheduled(
                        sprint.tenantId(),
                        sprint.productId(),
                        sprint.sprintId(),
                        sprint.name(),
                        sprint.goals(),
                        sprint.begins(),
                        sprint.ends()));

        // 然后，把发布好的冲刺实例返回
        return sprint;
    }
    /**
     * 开始发起讨论
     * @param aDiscussionInitiationId 讨论发起ID
     */
    public void startDiscussionInitiation(String aDiscussionInitiationId) {
        // 设置发起ID的条件：在讨论可用性方面，只有没准备就绪才发给一个讨论发起ID
        if (!this.discussion().availability().isReady()) {
            this.setDiscussionInitiationId(aDiscussionInitiationId);
        }
    }
    /**
     * 获取租户ID
     * @return 返回租户ID
     */
    public TenantId tenantId() {
        return this.tenantId;
    }
    /**
     * 私有构造器
     */
    private Product() {
        // 调用父构造器
        super();
        // 使用HashSet设置待办事项清单
        this.setBacklogItems(new HashSet<ProductBacklogItem>(0));
    }

    /**
     * 获取产品待办事项清单
     * @return 返回产品待办事项清单
     */
    private Set<ProductBacklogItem> backlogItems() {
        return this.backlogItems;
    }

    /**
     * 设置产品待办事项清单
     * @param backlogItems 产品待办事项清单
     */
    private void setBacklogItems(Set<ProductBacklogItem> backlogItems) {
        this.backlogItems = backlogItems;
    }

    /**
     * 设置内容描述
     * @param aDescription	内容描述
     */
    private void setDescription(String aDescription) {
        // 如果参数为空的情况下，则抛出 “必须提供内容描述”
        this.assertArgumentNotEmpty(aDescription, "The description must be provided.");
        // 如果参数长度超过500的情况下，则抛出 “内容描述不得超过500个字符。”
        this.assertArgumentLength(aDescription, 500, "Description must be 500 characters or less.");

        this.description = aDescription;
    }

    /**
     * 设置讨论（产品讨论）
     * @param aDiscussion 产品讨论值对象
     */
    private void setDiscussion(ProductDiscussion aDiscussion) {
        // 只有在产品讨论值对象有效的情况下，才可以设置；否则报错
        this.assertArgumentNotNull(aDiscussion, "The discussion is required even if it is unused.");

        this.discussion = aDiscussion;
    }
    /**
     * 设置讨论发起Id
     * @param aDiscussionInitiationId 讨论发起ID
     */
    private void setDiscussionInitiationId(String aDiscussionInitiationId) {
        // 只有在讨论发起ID是有效的情况下，才可以设置；否则报错
        if (aDiscussionInitiationId != null) {
            this.assertArgumentLength(
                    aDiscussionInitiationId,
                    100,
                    "Discussion initiation identity must be 100 characters or less.");
        }

        this.discussionInitiationId = aDiscussionInitiationId;
    }

    /**
     * 设置产品名
     * @param aName 产品名
     */
    private void setName(String aName) {
        // 只有在参数不为空的情况下，才可以设置；否则报错
        this.assertArgumentNotEmpty(aName, "The name must be provided.");
        // 只有在参数长度不大于100的情况下，才可以设置；否则报错
        this.assertArgumentLength(aName, 100, "The name must be 100 characters or less.");

        this.name = aName;
    }

    /**
     * 设置产品ID
     * @param aProductId 产品ID
     */
    private void setProductId(ProductId aProductId) {
        // 只有在参数有效的情况下，才可以设置；否则报错
        this.assertArgumentNotNull(aProductId, "The productId must be provided.");

        this.productId = aProductId;
    }

    /**
     * 设置产品经理ID
     * @param aProductOwnerId 产品经理ID
     */
    private void setProductOwnerId(ProductOwnerId aProductOwnerId) {
        // 只有在参数有效的情况下，才可以设置；否则报错
        this.assertArgumentNotNull(aProductOwnerId, "The productOwnerId must be provided.");
        // 只有在参数ＩＤ和产品经理ID相同的情况下，才可以设置；否则报错
        // 调用断言参数租户ID是否等于产品经理ID，如果不相等则抛出“productOwner必须具有相同的租户”
        this.assertArgumentEquals(this.tenantId(), aProductOwnerId.tenantId(), "The productOwner must have the same tenant.");

        this.productOwnerId = aProductOwnerId;
    }

    /**
     * 设置租户ID
     * @param aTenantId 租户ID
     */
    private void setTenantId(TenantId aTenantId) {
        // 只有在参数有效的情况下，才可以设置；否则报错
        this.assertArgumentNotNull(aTenantId, "The tenantId must be provided.");

        this.tenantId = aTenantId;
    }

    /**
     * 从四个维度比较（比较对象有效、类、租户ID、产品ID）
     * @param anObject 比较对象
     * @return 是否相等
     */
    @Override
    public boolean equals(Object anObject) {
        boolean equalObjects = false;

        if (anObject != null && this.getClass() == anObject.getClass()) {
            Product typedObject = (Product) anObject;
            equalObjects =
                    this.tenantId().equals(typedObject.tenantId()) &&
                            this.productId().equals(typedObject.productId());
        }

        return equalObjects;
    }

    /**
     * 逻辑重新覆盖
     * @return 返回最新的租户ID的哈希值+产品ID的哈希值+常量
     */
    @Override
    public int hashCode() {
        int hashCodeValue =
                + (2335 * 3)
                        + this.tenantId().hashCode()
                        + this.productId().hashCode();

        return hashCodeValue;
    }

    /**
     * 逻辑重新覆盖
     * @return 返回租户ID，产品ID，待办事项清单，内容描述，产品讨论，讨论发起ID，产品名，产品经理ID
     */
    @Override
    public String toString() {
        return "Product [tenantId=" + tenantId + ", productId=" + productId
                + ", backlogItems=" + backlogItems + ", description="
                + description + ", discussion=" + discussion
                + ", discussionInitiationId=" + discussionInitiationId
                + ", name=" + name + ", productOwnerId=" + productOwnerId + "]";
    }

}
